内存与计算平台之间有比较大的差异。为了可移植性,OpenCL定义一个抽象的内存模型,其目的是为了能让编程者写出的代码对应到供应商所提供的实际硬件内存上。内存模型描述了,平台为了OpenCL程序所外现的内存(系统)结构。内存模型需要定义,如何让执行单元看到对应值的方式。内存模型是保证OpenCL程序正确性的关键。
内存模型可以实现编程者所期望的功能,对应内存操作能保证其发生的顺序,以及内存中实际的数值(当读取操作返回时)。OpenCL内存一致性模型基于ISO C11编程语言的内存模型。第6章和第7章会详细讨论内存模型的内存,包括一致性内存模型和共享虚拟内存。我们只需要了解一下OpenCL中定义的不同内存类型,其中内存区域是对抽象内存模型的补足。了解了这些之后,我们就可以开始第一个OpenCL程序了。
3.5.1 内存对象OpenCL内核通常需要对输入和输出数据进行分类(例如,数组或多维矩阵)。程序执行前,需要保证输入数据能够在设备端访问到。为了将数据转移到设备端,首先做的事就是封装出一个内存对象。为了产生输出数据,需要开辟相应大小的空间,以及将开辟的空间封装成一个内存对象。OpenCL定义了三种内存类型:数组、图像和管道。
数组缓存
Buffer类型类似于C语言中的数据(使用malloc函数开辟),这种类型中数据在内存上是连续的。理论上,这种类型可以在设备端以指针的方式使用。OpenCL API clCreateBuffer()为这种类型分配内存,并返回一个内存对象。
cl_memclCreateBuffer( cl_context context, cl_mem_flags flags, size_t size, void *host_ptr, cl_int *errcode_ret)该API类似于C中malloc()函数,或C++中的new操作。创建一个数组需要知道其长度和创建在哪一个上下文对象上;创建之后,与该上下文对象相关联的设备就能看到这个内存对象。对于第二个标识参数,是用来指定设备端可对内存进行的操作,可以是“只读”、“只写”或“读写”。其他标识需要在数组创建的时候指定。比较简单的选项是使用主机端的指针来初始化一段数组。我们能看到的是OpenCL数组是与上下文对象进行关联,而非某个设备,所以数据转移的是在运行时确定的。数组转移到指定的设备上,或是从指定的设备转移到其他地方,都由OpenCL运行时根据数据的依赖性进行管理。
图像对象
图像也是OpenCL内存对象,其抽象了物理数据的存储,以便“设备指定”的访存优化。与数组不同,图像数组数据不能直接访问。因为相邻的数据并不保证在内存上连续存储。使用图像的目的就是为了发挥硬件空间局部性的优势,并且可以利用设备硬件加速的能力。
cl_memclCreateImage( cl_context context, cl_mem_flags flags, const cl_image_format *image_format, const cl_image_desc *image_desc, void *host_ptr, cl_int *errcode_ret)图像没有数据类型或维度,图像对象的创建需要通过描述符,让硬件了解这段内存数据的具体信息。图像对象中的每个元素通过格式描述符来表示(cl_image_format)。格式描述符用于描述图像元素在内存上是如何存储,以及使用通道的信息。通道序(channel order)指的是由多少个通道元素组成一个图像元素(例如,RGBA就是由四个通道值组成一个像素,其通道序为4),并且通道类型(channel type)指定了每个元素的大小。大小可以设置为1到4字节中的任意值,这样就能表现多种不同的格式(从整型到浮点)。其他数据元通过图像描述符(cl_image_desc)提供,其包括了图像的类型和维度。第4章我们会看到一个使用图像的例子。第6章和第7章,我们将详细的讨论图像的架构设计和评估。
为了支持图像类型,在设备端OpenCL C专门提供了用于读写图像数据的内置函数。硬件供应商可以通过这些函数,在底层单独对图像访问进行优化,或者利用硬件加速功能提高图像访存的速度。与数组相比,图像读写函数需要额外的参数,并且这些函数根据图像的具体数据类型进行使用。例如,read_imagef()函数就适用于读取浮点型的数值,read_imageui()函数就使用与读取无符号整型的数值。这些函数在使用的数据类型上有些不同,但在读取方面至少需要有一组访问坐标和一个采样器对象。采样器可以指定,设备访问到图像外部时,这些不存在的数据应该如何获取,是否使用差值,以及是否对坐标进行归一化。写入图像需要手动将数据转换成对应的存储数据格式(例如,对应的通道和对应的数据大小),目的坐标也需要手动的进行转换。
之前的OpenCL标准中,内核不允许对一个图像对象同时进行写入和读取。不过,OpenCL 2.0放松了这一要求,其提供的一系列同步操作,能让编程者安全的在同一内核中对同一图像对象进行读和写。
管道对象
管道内存对象就是一个数据元素(被称为packets)队列,其和其他队列一样,遵循FIFO(先进先出)的方式。一个管道对象具有一个写入末尾点,用于表示元素由这里插入;并且,有一个读取末尾点,用于表示元素由这里移除。要创建一个管道对象时,需要调用OpenCL API clCreatePipe(),这里需要提供包的大小和管道中可容纳包的最大数量(例如,创建时固定了管道中可容纳包的最大值)。函数clGetPipeInfo()可以返回管道中包的大小和整体大小(也就是可容纳包的最大值)。属性参数是一个保留参数,在OpenCL 2.0阶段,这个值只能传NULL。
cl_memclCreatePipe( cl_context context, cl_mem_flags flags, cl_uint pipe_packet_size, cl_uint pipe_max_packets, const cl_pipe_properties *properties, cl_int *errcode_ret)任意时间点,只能有一个内核向管道中存入包,并且只有一个内核从管道中读取包。为了支持“生产者-消费者”设计模式,一个内核与写入末尾点连接(生产者),同时另一个内核与读取末尾点连接(消费者)。同一个内核不能同时对一个管道进行读取和存入。
图像和管道都是不透明的数据结构,其只能通过OpenCL C的内置函数进行访问(比如,read_pipe()和write_pipe())。OpenCL C也提供相应的函数,可以保留管道的读取点和写入点。内置函数允许管道在工作组级别上进行访问,而不需要单独访问每个工作项,并且能在工作组级别上执行同步。第6章将会对管道进行